package com.tcy.game.angryrobots;

import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Pool;
import com.tcy.game.angryrobots.general.Colliders;
import com.tcy.game.angryrobots.general.Config;
import com.tcy.game.angryrobots.general.Grid;
import com.tcy.game.angryrobots.general.Pools;
import com.tcy.game.angryrobots.sprite.BaseShot;
import com.tcy.game.angryrobots.sprite.Captain;
import com.tcy.game.angryrobots.sprite.GameObject;
import com.tcy.game.angryrobots.sprite.Player;
import com.tcy.game.angryrobots.sprite.PlayerShot;
import com.tcy.game.angryrobots.sprite.Robot;
import com.tcy.game.angryrobots.sprite.RobotShot;

import static com.badlogic.gdx.math.MathUtils.*;
import static com.tcy.game.angryrobots.general.MathUtils.max;

/**
 * The <code>World</code> is the representation of the game world of <b>Very Angry Robots</b>. It knows nothing about how it will
 * be displayed, neither does it know about how the player is controlled, particle effects, sounds, nor anything else. It purely
 * knows about the {@link Player}, {@link Robot}s and the walls of the room that the player is in.
 * <p/>
 * Created by 80002023 on 2016/7/27.
 */
public class World {

    /**
     * The <code>FireCommand</code> interface is how the {@link World} is told that a {@link GameObject} wants to fire.
     */
    public static interface FireCommand {
        /**
         * Tells the {@link World} that a {@link GameObject} wants to fire. Note that <code>dx</code> and <code>dy</code> must be
         * normalised. The World does not have to fire just because it is asked to.
         *
         * @param firer the {@link GameObject} that wants to fire.
         * @param dx    the horizontal component of the bullet's direction.
         * @param dy    the vertical component of the bullet's direction.
         */
        void fire(GameObject firer, float dx, float dy);
    }

    // Maze proportions.
    private static final int VCELLS = Config.asInt("Maze.vCells", 3);
    private static final int HCELLS = Config.asInt("Maze.hCells", 5);

    // Wall sizes.
    public static final float WALL_HEIGHT = 0.25f;
    public static final float WALL_WIDTH = 6.0f;
    public static final float OUTER_WALL_ADJUST = WALL_HEIGHT;

    private static final int MAX_PLAYER_SHOTS = Config.asInt("Player.maxShots", 4);
    private static final int MAX_ROBOT_SHOTS = Config.asInt("Robot.maxShots", 6);
    private static final int MAX_ROBOTS = Config.asInt("Global.maxRobots", 12);
    private static final float PLAYER_DEAD_INTERVAL = Config.asFloat("Global.deadTime", 2.0f);
    private static final float FIRING_AMNESTY_INTERVAL = Config.asFloat("Global.amnestyTime", 2.0f);
    private static final float CAPTAIN_LURK_MULTIPLIER = Config.asFloat("Captain.lurkMultiplier", 2.0f);
    private static final float CAPTAIN_MIN_DELAY = Config.asFloat("Captain.minLurkTime", 2.0f);
    static final float FIRING_INTERVAL = Config.asFloat("Player.firingInterval", 0.25f);
    public static final float ROOM_TRANSITION_TIME = Config.asFloat("Global.roomTransitionTime", 0.5f);

    // Game states.
    public static final int RESETTING = 1;
    public static final int ENTERED_ROOM = 2;
    public static final int PLAYING = 3;
    public static final int PLAYER_DEAD = 4;

    private final Pool<PlayerShot> shotPool;
    private final Pool<Robot> robotPool;
    private final Pool<RobotShot> robotShotPool;
    private final Grid roomGrid;
    private final RoomBuilder roomBuilder;
    private final Rectangle roomBounds;
    private final float minX;
    private final float maxX;
    private final float minY;
    private final float maxY;
    final WorldNotifier notifier;
    private final DifficultyManager difficultyManager;
    private long roomSeed;
    private int roomX;
    private int roomY;
    private float playingTime;
    float nextFireTime;
    private int numRobotShots;
    private float robotShotSpeed;
    private int numRobots;
    private Vector2 playerPos;
    float now;
    private Array<Rectangle> doorRects;
    private Array<Rectangle> wallRects;
    private int doorPosition;
    private Player player;
    private Array<PlayerShot> playerShots;
    private Array<Robot> robots;
    private Array<RobotShot> robotShots;
    private Captain captain;
    private int state;
    private float stateTime;
    private Color robotColor;
    private boolean isPaused;
    private float pausedTime;

    public void pause() {
        isPaused = true;
        pausedTime = 0.0f;
    }

    public void resume() {
        isPaused = false;
    }

    public boolean isPaused() {
        return isPaused;
    }

    public float getPausedTime() {
        return pausedTime;
    }

    public int getState() {
        return state;
    }

    private void setState(int newState) {
        state = newState;
        stateTime = 0.0f;
    }

    public float getStateTime() {
        return stateTime;
    }

    public Color getRobotColor() {
        return robotColor;
    }

    public int getDoorPosition() {
        return doorPosition;
    }

    public Array<Rectangle> getDoorRects() {
        return doorRects;
    }

    public Array<Rectangle> getWallRects() {
        return wallRects;
    }

    public Rectangle getRoomBounds() {
        return roomBounds;
    }

    public Player getPlayer() {
        return player;
    }

    public Array<PlayerShot> getPlayerShots() {
        return playerShots;
    }

    public Array<Robot> getRobots() {
        return robots;
    }

    public Array<RobotShot> getRobotShots() {
        return robotShots;
    }

    public Captain getCaptain() {
        return captain;
    }

    /**
     * Adds another listener to the {@link World}.
     *
     * @param listener the listener.
     */
    public void addWorldListener(WorldListener listener) {
        notifier.addListener(listener);
    }

    public final FireCommand firePlayerShot = new FireCommand() {
        @Override
        public void fire(GameObject firer, float dx, float dy) {
            if (now >= nextFireTime) {
                addPlayerShot(dx, dy);
                nextFireTime = now + FIRING_INTERVAL;
            }
        }
    };

    void addPlayerShot(float dx, float dy) {
        if (state == PLAYING && playerShots.size < MAX_PLAYER_SHOTS) {
            PlayerShot shot = shotPool.obtain();
            shot.inCollision = false;
            float x = player.x + player.width / 2 - shot.width / 2;
            float y = player.y + player.height / 2 - shot.height / 2;
            shot.fire(x, y, dx, dy);
            playerShots.add(shot);
            notifier.onPlayerFired();
        }
    }

    public final FireCommand fireRobotShot = new FireCommand() {
        @Override
        public void fire(GameObject firer, float dx, float dy) {
            addRobotShot(firer, dx, dy);
        }
    };

    void addRobotShot(GameObject firer, float dx, float dy) {
        if (state == PLAYING && robotShots.size < numRobotShots && stateTime >= FIRING_AMNESTY_INTERVAL) {
            RobotShot shot = robotShotPool.obtain();
            shot.inCollision = false;
            shot.setOwner(firer);
            shot.setShotSpeed(robotShotSpeed);
            float x = firer.x + firer.width / 2 - shot.width / 2;
            float y = firer.y + firer.height / 2 - shot.height / 2;
            shot.fire(x, y, dx, dy);
            robotShots.add(shot);
            notifier.onRobotFired((Robot) firer);
        }
    }

    private final Colliders.RemovalHandler<Robot> robotRemovalHandler = new Colliders.RemovalHandler<Robot>() {
        @Override
        public void onRemove(Robot robot) {
            notifier.onRobotDestroyed(robot);
        }
    };

    private final Colliders.RemovalHandler<BaseShot> shotRemovalHandler = new Colliders.RemovalHandler<BaseShot>() {
        @Override
        public void onRemove(BaseShot shot) {
            notifier.onShotDestroyed(shot);
        }
    };

    private final Colliders.ColliderHandler<GameObject, GameObject> gameObjectCollisionHandler = new Colliders.ColliderHandler<GameObject, GameObject>() {
        @Override
        public void onCollision(GameObject t, GameObject u) {
            t.inCollision = true;
            u.inCollision = true;
        }
    };

    private final Colliders.ColliderHandler<Captain, GameObject> captainGameObjectCollisionHandler = new Colliders.ColliderHandler<Captain, GameObject>() {
        @Override
        public void onCollision(Captain captain, GameObject go) {
            go.inCollision = true;
        }
    };

    private final Colliders.ColliderHandler<PlayerShot, Robot> shotRobotCollisionHandler = new Colliders.ColliderHandler<PlayerShot, Robot>() {
        @Override
        public void onCollision(PlayerShot shot, Robot robot) {
            if (!robot.inCollision) {
                notifier.onRobotHit(robot);
            }
            shot.inCollision = true;
            robot.inCollision = true;
        }
    };

    private final Colliders.ColliderHandler<Robot, Robot> robotRobotCollisionHandler = new Colliders.ColliderHandler<Robot, Robot>() {
        @Override
        public void onCollision(Robot t, Robot u) {
            t.inCollision = true;
            u.inCollision = true;
        }
    };

    private final Colliders.SceneryHandler<Player> playerSceneryHandler = new Colliders.SceneryHandler<Player>() {
        @Override
        public void onCollision(Player player, Rectangle r) {
            player.inCollision = true;
        }
    };

    private final Colliders.SceneryHandler<GameObject> gameObjectSceneryHandler = new Colliders.SceneryHandler<GameObject>() {
        @Override
        public void onCollision(GameObject t, Rectangle r) {
            t.inCollision = true;
        }
    };

    /**
     * Constructs a new {@link World}.
     */
    public World(DifficultyManager difficultyManager) {
        this.difficultyManager = difficultyManager;
        notifier = new WorldNotifier();
        minX = 0;
        maxX = WALL_WIDTH * HCELLS;
        minY = 0;
        maxY = WALL_WIDTH * VCELLS;
        roomBounds = new Rectangle(minX, minY, maxX - minX, maxY - minY);
        player = new Player();
        captain = new Captain();
        playerPos = new Vector2();
        roomBuilder = new RoomBuilder(HCELLS, VCELLS);
        roomGrid = new Grid(HCELLS * 2, VCELLS * 2, maxX, maxY);

        shotPool = new Pool<PlayerShot>(MAX_PLAYER_SHOTS, MAX_PLAYER_SHOTS) {
            @Override
            protected PlayerShot newObject() {
                return new PlayerShot();
            }
        };

        robotPool = new Pool<Robot>(MAX_ROBOTS, MAX_ROBOTS) {
            @Override
            protected Robot newObject() {
                return new Robot();
            }
        };

        robotShotPool = new Pool<RobotShot>(MAX_ROBOT_SHOTS, MAX_ROBOT_SHOTS) {
            @Override
            protected RobotShot newObject() {
                return new RobotShot();
            }
        };
    }

    /**
     * Resets the {@link World} to its starting state.
     */
    public void reset() {
        setState(RESETTING);
    }

    /**
     * Called when the {@link World} is to be updated.
     *
     * @param delta the time in seconds since the last render.
     */
    public void update(float delta) {
        if (!isPaused) {
            now += delta;
            stateTime += delta;
            switch (state) {
                case RESETTING:
                    updateResetting();
                    break;
                case ENTERED_ROOM:
                    updateEnteredRoom();
                    break;
                case PLAYING:
                    updatePlaying(delta);
                    break;
                case PLAYER_DEAD:
                    updatePlayerDead(delta);
                    break;
            }
        } else {
            pausedTime += delta;
        }
    }

    private void updateResetting() {
        notifier.onWorldReset();
        roomSeed = System.currentTimeMillis();
        roomX = 0;
        roomY = 0;
        populateRoom(DoorPositions.MIN_Y);
    }

    private void updateEnteredRoom() {
        if (stateTime >= ROOM_TRANSITION_TIME) {
            setState(PLAYING);
        }
    }

    private void updatePlaying(float delta) {
        player.update(delta);
        updateMobiles(delta);
        checkForCollisions();
        checkForLeavingRoom();
    }

    private void updatePlayerDead(float delta) {
        updateMobiles(delta);
        checkForCollisions();
        if (now >= playingTime) {
            resetRoom();
            setState(PLAYING);
        }
    }

    private void populateRoom(int doorPos) {
        doorPosition = doorPos;
        setRandomSeedFromRoom();
        createMaze();
        placePlayer();
        createRobots();
        placeCaptain();
        createPlayerShots();
        createRobotShots();
        setState(ENTERED_ROOM);
        notifier.onEnteredRoom(now, numRobots);
    }

    private void createMaze() {
        roomBuilder.build(doorPosition);
        wallRects = roomBuilder.getWalls();
        doorRects = roomBuilder.getDoors();
        roomGrid.clear();
        for (Rectangle r : wallRects) {
            roomGrid.add(r);
        }
        for (Rectangle r : doorRects) {
            roomGrid.add(r);
        }
    }

    private void setRandomSeedFromRoom() {
        long seed = roomSeed + ((roomX & 0xff) | ((roomY & 0xff) << 8));
        random.setSeed(seed);
    }

    private void placePlayer() {
        player.inCollision = false;

        switch (doorPosition) {

            case DoorPositions.MIN_X:
                player.x = minX + player.width / 2;
                player.y = (maxY + minY) / 2 - player.height / 2;
                break;

            case DoorPositions.MAX_X:
                player.x = maxX - player.width - player.width / 2;
                player.y = (maxY + minY) / 2 - player.height / 2;
                break;

            case DoorPositions.MAX_Y:
                player.x = (maxX + minX) / 2 - player.width / 2;
                player.y = maxY - player.height - player.height / 4;
                break;

            case DoorPositions.MIN_Y:
            default:
                player.x = (maxX + minX) / 2 - player.width / 2;
                player.y = minY + player.height / 4;
                break;
        }

        player.setState(Player.FACING_RIGHT);
        notifier.onPlayerSpawned();
    }

    private void placeCaptain() {
        captain.inCollision = false;
        captain.setState(Captain.LURKING);
        captain.activateAfter(max(CAPTAIN_MIN_DELAY, CAPTAIN_LURK_MULTIPLIER * robots.size));
        captain.setPlayer(player);

        switch (doorPosition) {
            case DoorPositions.MIN_X:
                captain.x = minX - 2 * captain.width;
                captain.y = (maxY + minY) / 2 - captain.height / 2;
                break;

            case DoorPositions.MAX_X:
                captain.x = maxX + captain.width;
                captain.y = (maxY + minY) / 2 - captain.height / 2;
                break;

            case DoorPositions.MAX_Y:
                captain.x = (maxX + minX) / 2 - captain.width / 2;
                captain.y = maxY + captain.height;
                break;

            case DoorPositions.MIN_Y:
            default:
                captain.x = (maxX + minX) / 2 - captain.width / 2;
                captain.y = minY - 2 * captain.height;
                break;
        }
    }

    private void createRobots() {
        robotColor = difficultyManager.getRobotColor();
        numRobots = difficultyManager.getNumberOfRobots();
        numRobotShots = difficultyManager.getNumberOfRobotShots();
        robotShotSpeed = difficultyManager.getRobotShotSpeed();

        final float minXSpawn = minX + WALL_HEIGHT;
        final float minYSpawn = minY + WALL_HEIGHT;
        final float maxXSpawn = maxX - WALL_HEIGHT;
        final float maxYSpawn = maxY - WALL_HEIGHT;
        robots = Pools.makeArrayFromPool(robots, robotPool, MAX_ROBOTS);
        playerPos.set(player.x, player.y);
        for (int i = 0; i < numRobots; i++) {
            Robot robot = robotPool.obtain();
            robot.inCollision = false;
            do {
                robot.x = random(minXSpawn, maxXSpawn - robot.width);
                robot.y = random(minYSpawn, maxYSpawn - robot.height);
            } while (!canSpawnHere(robot));
            robot.setRespawnPoint(robot.x, robot.y);
            robot.setPlayer(player);
            robot.setWalls(wallRects);
            robot.setFireCommand(fireRobotShot);
            robots.add(robot);
        }
    }

    private boolean canSpawnHere(Robot robot) {
        return !(intersectsWalls(robot) || intersectsDoors(robot) || intersectsRobots(robot) || playerPos.dst(robot.x, robot.y) < WALL_WIDTH);
    }

    private boolean intersectsWalls(Robot robot) {
        return Colliders.intersects(robot.bounds(), wallRects);
    }

    private boolean intersectsDoors(Robot robot) {
        return Colliders.intersects(robot.bounds(), doorRects);
    }

    private boolean intersectsRobots(Robot robot) {
        for (int i = 0; i < robots.size; i++) {
            if (robot.boundsIntersect(robots.get(i))) {
                return true;
            }
        }
        return false;
    }

    private void createPlayerShots() {
        playerShots = Pools.makeArrayFromPool(playerShots, shotPool, MAX_PLAYER_SHOTS);
    }

    private void createRobotShots() {
        robotShots = Pools.makeArrayFromPool(robotShots, robotShotPool, MAX_ROBOT_SHOTS);
    }

    private void updateMobiles(float delta) {
        updateCaptain(delta);
        update(robots, delta);
        update(playerShots, delta);
        update(robotShots, delta);
    }

    private void resetRoom() {
        placePlayer();
        placeCaptain();
        for (Robot robot : robots) {
            robot.respawn();
        }
        robotShots.clear();
        playerShots.clear();
    }

    private void updateCaptain(float delta) {
        captain.update(delta);
        if (captain.stateTime == 0.0f && captain.state == Captain.CHASING) {
            doCaptainActivated();
        }
    }

    private void update(Array<? extends GameObject> gos, float delta) {
        for (GameObject go : gos) {
            go.update(delta);
        }
    }

    private void checkForCollisions() {
        checkMobileMobileCollisions();
        checkMobileSceneryCollisions();
        removeMarkedMobiles();
        if (state == PLAYING && player.inCollision) {
            doPlayerHit();
        }
    }

    private void checkMobileMobileCollisions() {
        Colliders.collide(player, robots, gameObjectCollisionHandler);
        Colliders.collide(player, robotShots, gameObjectCollisionHandler);
        Colliders.collide(captain, player, captainGameObjectCollisionHandler);
        Colliders.collide(playerShots, robots, shotRobotCollisionHandler);
        Colliders.collide(playerShots, robotShots, gameObjectCollisionHandler);
        Colliders.collide(robots, robotRobotCollisionHandler);
        Colliders.collide(robotShots, robots, gameObjectCollisionHandler);
        Colliders.collide(robotShots, gameObjectCollisionHandler);
        Colliders.collide(captain, robots, captainGameObjectCollisionHandler);
        Colliders.collide(captain, playerShots, captainGameObjectCollisionHandler);
        Colliders.collide(captain, robotShots, captainGameObjectCollisionHandler);
    }

    private void checkMobileSceneryCollisions() {
        Colliders.collide(player, roomGrid.get(player.bounds()), playerSceneryHandler);
        markSceneryCollisions(robots, gameObjectSceneryHandler);
        markSceneryCollisions(playerShots, gameObjectSceneryHandler);
        markSceneryCollisions(robotShots, gameObjectSceneryHandler);
    }

    private <U extends GameObject, T extends U> void markSceneryCollisions(Array<T> gos, Colliders.SceneryHandler<U> handler) {
        for (int i = 0; i < gos.size; i++) {
            T go = gos.get(i);
            Colliders.collide(go, roomGrid.get(go.bounds()), handler);
        }
    }

    private void removeMarkedMobiles() {
        Colliders.removeOutOfBounds(shotPool, playerShots, roomBounds);
        Colliders.removeOutOfBounds(robotShotPool, robotShots, roomBounds);
        Colliders.removeMarkedCollisions(shotPool, playerShots, shotRemovalHandler);
        Colliders.removeMarkedCollisions(robotPool, robots, robotRemovalHandler);
        Colliders.removeMarkedCollisions(robotShotPool, robotShots, shotRemovalHandler);
    }

    private void checkForLeavingRoom() {
        int newDoor = -1;
        if (player.x + player.width / 2 < minX) {
            roomX--;
            newDoor = DoorPositions.MAX_X;
        } else if (player.x + player.width / 2 > maxX) {
            roomX++;
            newDoor = DoorPositions.MIN_X;
        } else if (player.y + player.height / 2 < minY) {
            roomY--;
            newDoor = DoorPositions.MAX_Y;
        } else if (player.y + player.height / 2 > maxY) {
            roomY++;
            newDoor = DoorPositions.MIN_Y;
        }
        if (newDoor != -1) {
            doLeftRoom(newDoor);
        }
    }

    private void doPlayerHit() {
        notifier.onPlayerHit();
        setState(PLAYER_DEAD);
        playingTime = now + PLAYER_DEAD_INTERVAL;
    }

    private void doLeftRoom(int newDoor) {
        notifier.onExitedRoom(now, robots.size);
        populateRoom(newDoor);
    }

    private void doCaptainActivated() {
        notifier.onCaptainActivated(now);
    }
}
